spb/autoscholar
Public
1import { getRun } from "@autoscholar/core";2import { publicationDecisions, qualityGateResults, reviewFindings } from "@autoscholar/db";3import { asc, desc, eq } from "drizzle-orm";4import Link from "next/link";5import { notFound } from "next/navigation";6import { StatusBadge } from "@/components/StatusBadge";7import { fmtDateTime } from "@/lib/format";8import { db } from "@/lib/server/db";9import { OverrideForm } from "./OverrideForm";1011export const dynamic = "force-dynamic";12const NON_OVERRIDABLE = [13 "worker.real",14 "references.verified",15 "pdf.valid",16 "secrets.none",17 "attempt.final",18 "checksums.valid",19 "results.consistency",20 "data.provenance",21 "claims.traced",22 "contract.artifacts",23 "disclosure.present",24];2526export default async function QualityDetail({ params }: { params: Promise<{ runId: string }> }) {27 const { runId } = await params;28 const d = db();29 let data: Awaited<ReturnType<typeof getRun>>;30 try {31 data = await getRun(d, runId);32 } catch {33 notFound();34 }35 const gates = await d36 .select()37 .from(qualityGateResults)38 .where(eq(qualityGateResults.runId, runId))39 .orderBy(desc(qualityGateResults.createdAt), asc(qualityGateResults.gate));40 const evalIds = [...new Set(gates.map((g) => g.evaluationId))];41 const findings = await d42 .select()43 .from(reviewFindings)44 .where(eq(reviewFindings.runId, runId))45 .orderBy(asc(reviewFindings.round));46 const decisions = await d47 .select()48 .from(publicationDecisions)49 .where(eq(publicationDecisions.runId, runId))50 .orderBy(desc(publicationDecisions.createdAt));51 const latest = gates.filter((g) => g.evaluationId === evalIds[0]);52 const failedHard = latest.filter((g) => g.severity === "hard" && g.status === "fail");53 const blocked = failedHard.filter((g) => NON_OVERRIDABLE.includes(g.gate));54 return (55 <div className="space-y-6">56 <header>57 <p className="eyebrow">58 <Link href="/admin/quality">Quality</Link>59 </p>60 <h1 className="title-2 mt-1">{data.run.title}</h1>61 <p className="muted flex items-center gap-2 text-sm">62 <StatusBadge status={data.run.status} /> policy {data.run.qualityPolicyVersion} ·{" "}63 <Link href={`/admin/runs/${runId}`}>run</Link> ·{" "}64 <a href={`/api/admin/runs/${runId}/artifacts/review/qa-report.json`}>qa-report.json</a>65 </p>66 </header>67 {gates.length === 0 && (68 <div className="surface p-8 text-center muted">69 No evaluation yet. The scheduler evaluates runs in quality_check.70 </div>71 )}72 {evalIds.map((id, i) => {73 const g = gates.filter((x) => x.evaluationId === id);74 return (75 <section key={id} className="surface p-4" aria-labelledby={`e-${id}`}>76 <h2 id={`e-${id}`} className="title-3">77 Evaluation {i === 0 ? "(latest)" : ""}{" "}78 <span className="subtle font-mono text-xs">79 {id.slice(0, 8)} · {fmtDateTime(g[0]?.createdAt)}80 </span>81 </h2>82 <p className="muted text-xs">83 {g.filter((x) => x.status === "pass").length} pass ·{" "}84 {g.filter((x) => x.status === "fail").length} fail ·{" "}85 {g.filter((x) => x.status === "warn").length} warn ·{" "}86 {g.filter((x) => x.status === "skip").length} skip87 </p>88 <table className="table mt-2">89 <thead>90 <tr>91 <th>Gate</th>92 <th>Severity</th>93 <th>Status</th>94 <th>Message</th>95 <th>Evidence</th>96 </tr>97 </thead>98 <tbody>99 {g.map((x) => (100 <tr key={x.id}>101 <td className="font-mono text-xs">102 {x.gate}103 {NON_OVERRIDABLE.includes(x.gate) && (104 <span className="subtle block text-[10px]">non-overridable</span>105 )}106 </td>107 <td>{x.severity}</td>108 <td>109 <StatusBadge status={x.status} />110 </td>111 <td className="text-xs">{x.message}</td>112 <td className="text-xs">113 {Object.keys(x.evidence).length ? (114 <details>115 <summary className="cursor-pointer subtle">show</summary>116 <pre className="max-w-md whitespace-pre-wrap">117 {JSON.stringify(x.evidence, null, 1)}118 </pre>119 </details>120 ) : (121 "—"122 )}123 </td>124 </tr>125 ))}126 </tbody>127 </table>128 </section>129 );130 })}131 {findings.length > 0 && (132 <section className="surface p-4">133 <h2 className="title-3">Reviewer findings</h2>134 <ul className="mt-2 space-y-2 text-sm">135 {findings.map((f) => (136 <li key={f.id}>137 <StatusBadge status={f.severity} /> <strong>{f.title}</strong>{" "}138 <span className="subtle text-xs">139 round {f.round} · {f.category} · {f.resolution}140 </span>141 <p className="muted text-xs">{f.detail}</p>142 {f.proposedTest && <p className="text-xs">Proposed test: {f.proposedTest}</p>}143 </li>144 ))}145 </ul>146 </section>147 )}148 <section className="surface p-4">149 <h2 className="title-3">Publication decisions</h2>150 {decisions.length === 0 ? (151 <p className="muted text-sm">None.</p>152 ) : (153 <ul className="mt-2 space-y-1 text-sm">154 {decisions.map((x) => (155 <li key={x.id}>156 <StatusBadge157 status={x.decision === "publish" ? "pass" : x.decision === "reject" ? "fail" : "warn"}158 />{" "}159 <strong>{x.decision}</strong> by {x.decidedBy} ·{" "}160 <span className="subtle text-xs">{fmtDateTime(x.createdAt)}</span>161 {x.override && <span className="badge ml-1">override</span>}162 <p className="muted text-xs">{x.rationale}</p>163 </li>164 ))}165 </ul>166 )}167 </section>168 {data.run.status === "quality_failed" && (169 <section className="surface p-4">170 <h2 className="title-3">Authorized override</h2>171 {blocked.length > 0 ? (172 <p className="text-sm" style={{ color: "var(--color-danger-500)" }}>173 Cannot override: scientific blockers failed ({blocked.map((b) => b.gate).join(", ")}). Fix the174 research and retry the run instead.175 </p>176 ) : (177 <OverrideForm runId={runId} gates={failedHard.map((x) => x.gate)} />178 )}179 </section>180 )}181 </div>182 );183}184